/**
  ******************************************************************************
  * @author     Chris
  * @since      2024/6/10 10:31
  *
  * @file       hd_oled.c
  * @brief      OLED hardware driver.
  *
  * @note       This file contains the hardware driver for the OLED.
  *
  * @warning    None.
  ******************************************************************************
  * Change Logs:
  *   Date           Author       Notes
  *   2024-06-10     Chris        the first version
  *
  ******************************************************************************
  */

#include <stdio.h>
#include <stdarg.h>
#include "hd_oled.h"


/** region OLED init config param */
const static uint8_t OLED_INIT_CONFIG[] = {
        0xAE, // 关闭显示
        0xD5, // 设置时钟分频因子,震荡频率
        0x80, // [3:0],分频因子;[7:4],震荡频率
        0xA8, // 设置驱动路数
        0X3F, // 默认0X3F(1/64)
        0xD3, // 设置显示偏移
        0X00, // 默认为0
        0x40, // 设置显示开始行 [5:0],行数.
        0x8D, // 电荷泵设置
        0x14, // bit2，开启/关闭
        0x20, // 设置内存地址模式
        0x02, // [1:0],00，列地址模式;01，行地址模式;10,页地址模式;默认10;
        0xA1, // 段重定义设置,bit0:0,0->0;1,0->127;
        0xC8, // 设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数
        0xDA, // 设置COM硬件引脚配置
        0x12, // [5:4]配置
        0x81, // 对比度设置
        0xEF, // 1~255;默认0X7F (亮度设置,越大越亮)
        0xD9, // 设置预充电周期
        0xf1, // [3:0],PHASE 1;[7:4],PHASE 2;
        0xDB, // 设置 VCOMH 电压倍率
        0x30, // [6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc;
        0xA4, // 全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)
        0xA6, // 设置显示方式;bit0:1,反相显示;0,正常显示
        0xAF, // 开启显示
};
/** endregion */

/**
 * Physically, OLED Graphic bits is [128][64](x * y).
 * Logically, OLED Graphic Memory is 8 * 128(page * segment). 128 * 8 is okay, except that it's
 * not convenient to display.
 *
 * The OLED_GRAPHIC_MEMORY[0][1] means the area of [8][0] ~ [15][7] at OLED Graphic bits [128][64].
 * --------------------------------------------------------------------------------------------
 * |   128x64    | c0 c1 c2 c3 c4 c5 c6 c7 | c8 c9 c10 c11 c12 c13 c14 c15 | c16 c17.... c127 |
 * |-------|-----|-------------------------|-------------------------------|------------------|
 * |       |  r0 |                         |                               |                  |
 * |       |  r1 |                         |                               |                  |
 * |       |  r2 |                         |                               |                  |
 * | PAGE0 |  r3 |       SEG0              |             SEG1              | SEG2 ... SEG127  |
 * |       |  r4 |                         |                               |                  |
 * |       |  r5 |                         |                               |                  |
 * |       |  r6 |                         |                               |                  |
 * |       |  r7 |                         |                               |                  |
 * |-------|---- |---------------------------------------------------------|------------------|
 * |       |  r8 |                         |                               |                  |
 * |       |  r9 |                         |                               |                  |
 * |       | r10 |                         |                               |                  |
 * | PAGE1 | r11 |                         |                               |                  |
 * |       | r12 |                         |                               |                  |
 * |       | r13 |                         |                               |                  |
 * |       | r14 |                         |                               |                  |
 * |       | r15 |                         |                               |                  |
 * |-------|---- |---------------------------------------------------------|------------------|
 * |       | r16 |                         |                               |                  |
 * | PAGE2 | r17 |                         |                               |                  |
 * |  .    | .   |                         |                               |                  |
 * |  .    | .   |                         |                               |                  |
 * |  .    | .   |                         |                               |                  |
 * | PAGE7 | r63 |                         |                               |                  |
 * |-------------------------------------------------------------------------------------------
 */
uint8_t OLED_GRAPHIC_MEMORY[8][128];



static uint32_t OLED_Pow(uint32_t X, uint32_t Y);


/**
  * @brief  OLED Init
  *
  * @note
  * @param  this OLED Object
  * @retval
  */
static void OLED_Init(OLED *this) {
    // IIC or SPI protocol GPIO Init
    this->oledProtocol->init(this->oledProtocol);
    // OLED init config
    this->oledProtocol->write(this->oledProtocol, OLED_I2C_ADDRESS, OLED_I2C_CMD,
                              (uint8_t *) OLED_INIT_CONFIG, sizeof(OLED_INIT_CONFIG) / sizeof(OLED_INIT_CONFIG[0]));
}

/**
  * @brief  OLED clear display
  *
  * @note   Turn off all display pixel at mode OLED_MODE_POSITIVE.Turn on all display
  *         pixel at mode OLED_MODE_NEGATIVE;
  * @param  this OLED Object
  * @retval
  */
static void OLED_Clear(OLED *this) {
    for (int page = 0; page < 8; ++page)
        for (int seg = 0; seg < 128; ++seg)
            OLED_GRAPHIC_MEMORY[page][seg] = this->mode == OLED_MODE_POSITIVE ? 0x00 : 0xFF;
}

/**
  * @brief  OLED display all pixel
  *
  * @note   Turn on all display pixel at mode OLED_MODE_POSITIVE.Turn off all display
  *         pixel at mode OLED_MODE_NEGATIVE;
  * @param  this OLED Object
  */
static void OLED_Full(OLED *this) {
    for (int page = 0; page < 8; ++page)
        for (int seg = 0; seg < 128; ++seg)
            OLED_GRAPHIC_MEMORY[page][seg] = this->mode == OLED_MODE_POSITIVE ? 0xFF : 0x00;
}

/**
  * @brief  OLED display a character
  *
  * @note
  * @param  x   Horizontal coordinates, [0:63]
  * @param  y   Vertical coordinates, [0:127]
  * @param  chr character of ASCII
  * @retval
  */
static uint8_t OLED_ShowChar(OLED *this, uint8_t x, uint8_t y, char chr) {
    /**
     * The bytes of the character at display = PAGE(round up) * SEG, for example:
     * | Height * Width | display pixel | Graphic Memory(PAGE x SEG) | Bytes |
     * | 12 * 6         |   12 * 6      |          2 * 6             | 12    |
     * | 16 * 8         |   16 * 8      |          2 * 8             | 16    |
     * | 24 * 12        |   24 * 12     |          3 * 12            | 36    |
     * | 14 * 7         |   14 * 7      |          2 * 7             | 14    |
     */
    uint8_t chrByteSize = (this->size / 8 + (this->size % 8 ? 1 : 0)) * (this->size / 2);
    // baseY as a benchmark
    const uint8_t baseY = y;
    // how many bytes does ascii character has, default 1
    uint8_t chrBytes;
    // dot matrix array. a ascii font has max 36 bytes at 24x12 pixel.
    uint8_t dotMatrix[chrByteSize];
    // get dot matrix data from any storage
    chrBytes = this->oledFont->getFontDotMatrix(this->oledFont, dotMatrix, (uint8_t *) &chr, this->size);

    for (int i = 0; i < chrByteSize; ++i) {
        uint8_t chrOfDisplay = dotMatrix[i];

        /**
         * For example, we write a character that have a 12*6 size to coordinate (0,0):
         * /--------------------------------------
         * |      | SEG0 | SET1 |
         * |------|-------------------------------
         * | bit0 |  |   |  |   |
         * | bit1 |  V   |  V   |
         * | bit2 |      |      |
         * | bit3 |  |   |  |   |
         * | bit4 |  V   |  V   |
         * | bit5 |      |      |
         * | bit6 |  |   |  |   |
         * | bit7 |  V   |  V   |
         * |------------------- --------------------
         * | bit8 |      |      |
         * | bit9 |  |   |  |   |
         * | bit10|  V   |  V   |
         * | bit11|      ^      ^
         * | bit12|  ->-/|  ->-/|
         * |----------------------------------------------
         * | bit13|      |      |
         * | bit14|      |      |
         * | bit15|      |      |
         * | .    |      |      |
         * | .    |      |      |
         * | .    |      |      |
         * \------------------------------------------
         * It can be seen from the above, we write bit0~bit7 of a byte at first, and then,
         * we write next segment immediately after bit8~bit11 is written.
         */
        for (int bit = 0; bit < 8; ++bit) {
            this->drawPoint(x, y, this->mode ? (chrOfDisplay & 0x01) : ((chrOfDisplay & 0x01) ? 0 : 1));
            chrOfDisplay >>= 1;
            y++;
            if (y - baseY >= this->size) {
                y = baseY;
                x++;
                break;
            }
        }
    }

    return chrBytes;
}

/**
  * @brief  OLED display string
  *
  * @note   one by one display through @ref OLED_ShowChar() function
  * @param  x   Horizontal coordinates, [0:63]
  * @param  y   Vertical coordinates, [0:127]
  * @param  str string
  * @retval
  */
static void OLED_ShowString(OLED *this, uint8_t x, uint8_t y, char *str) {
    while (*str != '\0') {
        if (*str > 0x80) {
            uint8_t len = this->showChinese(this, x, y, str);
            str += len;
            x += this->size;
        } else {
            uint8_t len = this->showChar(this, x, y, *str);
            str += len;
            x += this->size / 2;
        }
    }
}

/**
  * @brief  OLED display a unsigned integer in decimal
  *
  * @note
  * @param  x   Horizontal coordinates, [0:63]
  * @param  y   Vertical coordinates, [0:127]
  * @param  num unsigned integer
  * @param  len the number of characters on the screen
  * @retval
  */
static void OLED_ShowNum(OLED *this, uint8_t x, uint8_t y, uint32_t num, uint8_t len) {
    uint8_t head = 0;
    for (int i = 0; i < len; ++i) {
        uint8_t temp = num / OLED_Pow(10, len - i - 1) % 10;
        if (head == 0) {
            if (temp == 0) {
                this->showChar(this, x + this->size / 2 * i, y, '0');
                continue;
            } else { head = 1; }
        }

        this->showChar(this, x + this->size / 2 * i, y, temp + '0');
    }
}

/**
  * @brief  OLED display a signed integer in decimal
  *
  * @note
  * @param  x   Horizontal coordinates, [0:63]
  * @param  y   Vertical coordinates, [0:127]
  * @param  num signed integer
  * @param  len the number of characters on the screen
  * @retval
  */
static void OLED_ShowSignedNum(OLED *this, uint8_t x, uint8_t y, int32_t num, uint8_t len) {
    this->showChar(this, x, y, num >= 0 ? '+' : '-');
    uint32_t tempNum = num >= 0 ? num : -num;
    x += this->size / 2;

    for (uint8_t i = 0; i < len; i++) {
        this->showChar(this, x + i * this->size / 2, y, tempNum / OLED_Pow(10, len - i - 1) % 10 + '0');
    }
}

/**
  * @brief  OLED display a positive integer in hexadecimal
  *
  * @note
  * @param  x   Horizontal coordinates, [0:63]
  * @param  y   Vertical coordinates, [0:127]
  * @param  num hexadecimal
  * @param  len the number of characters on the screen
  */
static void OLED_ShowHexNum(OLED *this, uint8_t x, uint8_t y, uint32_t num, uint8_t len) {
    uint8_t singleNumber;

    for (uint8_t i = 0; i < len; i++) {
        singleNumber = num / OLED_Pow(16, len - i - 1) % 16;
        this->showChar(this, x + i * this->size / 2, y,
                        singleNumber < 10 ? singleNumber + '0' : singleNumber - 10 + 'A');
    }
}

/**
  * @brief  OLED display a binary positive integer
  *
  * @note
  * @param  x   Horizontal coordinates, [0:63]
  * @param  y   Vertical coordinates, [0:127]
  * @param  num binary
  * @param  len the number of characters on the screen
  */
static void OLED_ShowBinNum(OLED *this, uint8_t x, uint8_t y, uint32_t num, uint8_t len) {
    for (uint8_t i = 0; i < len; i++) {
        this->showChar(this, x + i * this->size / 2, y, num / OLED_Pow(2, len - i - 1) % 2 + '0');
    }
}

/**
  * @brief  OLED display a floating number
  *
  * @note
  * @param  x   Horizontal coordinates, [0:63]
  * @param  y   Vertical coordinates, [0:127]
  * @param  num binary
  * @param  len the number of characters on the screen
 */
static void OLED_ShowFloatNum(OLED *this, uint8_t x, uint8_t y, float num, uint8_t len) {
    const uint16_t numOfHundred = (uint16_t) (num * 100);
    for (uint8_t i = 0; i < len; i++) {
        uint8_t temp = (numOfHundred / OLED_Pow(10, len - i - 1)) % 10;
        if (i == (len - 2)) {
            this->showChar(this, x + (len - 2) * this->size / 2, y, '.');
            i++;
            len += 1;
        }

        this->showChar(this, x + i * this->size / 2, y, temp + '0');
    }
}

/**
  * @brief  OLED display a chinese
  *
  * @note
  * @param  x   Horizontal coordinates, [0:63]
  * @param  y   Vertical coordinates, [0:127]
  * @param  cn  character of chinese
  * @retval
  */
static uint8_t OLED_showChinese(OLED *this, uint8_t x, uint8_t y, char* cn) {
    /**
     * The bytes of the character at display = PAGE(round up) * SEG, for example:
     * | Height * Width | display pixel | Graphic Memory(PAGE x SEG) | Bytes |
     * |    12 * 12     |    12 * 12    |          2 * 12            |   24  |
     * |    16 * 16     |    16 * 16    |          2 * 16            |   32  |
     * |    24 * 24     |    24 * 24    |          3 * 24            |   72  |
     */
    uint8_t chrByteSize = (this->size / 8 + (this->size % 8 ? 1 : 0)) * this->size;
    // baseY as a benchmark
    const uint8_t baseY = y;
    // get chinese font dot matrix, and return the count(2 or 3) of bytes that a chinese font includes.
    uint8_t chineseBytes;
    // dot matrix array. a chinese font has max 72 bytes at 24x24 pixel.
    uint8_t dotMatrix[chrByteSize];

    chineseBytes = this->oledFont->getFontDotMatrix(this->oledFont, dotMatrix, (uint8_t *) cn, this->size);

    for (int i = 0; i < chrByteSize; ++i) {
        uint8_t chrOfDisplay = dotMatrix[i];

        /**
         * For example, we write a character that have a 12*6 size to coordinate (0,0):
         * /--------------------------------------
         * |      | SEG0 | SEG1 |
         * |------|-------------------------------
         * | bit0 |  |   |   |  |
         * | bit1 |  V   |   V  |
         * | bit2 |      |      |
         * | bit3 |  |   |   |  |
         * | bit4 |  V   |   V  |
         * | bit5 |      |      |
         * | bit6 |  |   |   |  |
         * | bit7 |  V   |   V  |
         * |----------------------------------------
         * | bit8 |      |      |
         * | bit9 |  |   |   |  |
         * | bit10|  V   |   V  |
         * | bit11|      ^      ^
         * | bit12|  --> |   -> |
         * |----------------------------------------------
         * | bit13|      |      |
         * | bit14|      |      |
         * | bit15|      |      |
         * | .    |      |      |
         * | .    |      |      |
         * | .    |      |      |
         * \------------------------------------------
         * It can be seen from the above, we write bit0~bit7 of a byte at first, and then,
         * we write next segment immediately after bit8~bit11 is written.
         */
        for (uint8_t mask = 0x01; mask != 0; mask <<= 1) {
            this->drawPoint(x, y, this->mode ? (chrOfDisplay & mask) : ((chrOfDisplay & mask) ? 0 : 1));
            y++;
            if (y - baseY >= this->size) {
                y = baseY;
                x++;
                break;
            }
        }
    }

    return chineseBytes;
}

/**
  * @brief  OLED display a image
  *
  * @note
  * @param  x       Horizontal coordinates, [0:63]
  * @param  y       Vertical coordinates, [0:127]
  * @param  width   image width
  * @param  height  image height
  * @retval
  */
static void OLED_showImage(OLED *this, uint8_t x, uint8_t y,
                           uint8_t width, uint8_t height, const uint8_t *dotMatrix) {
    // The bytes of the character at display = PAGE(round up) * SEG
    uint32_t chrByteSize = (height / 8 + (height % 8 ? 1 : 0)) * width;
    // baseY as a benchmark
    const uint8_t baseY = y;

    for (int i = 0; i < chrByteSize; ++i) {
        uint8_t chrOfDisplay = dotMatrix[i];

        /**
         * For example, we write a character that have a 12*6 size to coordinate (0,0):
         * /--------------------------------------
         * |      | SEG0 | SEG1 |
         * |------|-------------------------------
         * | bit0 |  |   |   |  |
         * | bit1 |  V   |   V  |
         * | bit2 |      |      |
         * | bit3 |  |   |   |  |
         * | bit4 |  V   |   V  |
         * | bit5 |      |      |
         * | bit6 |  |   |   |  |
         * | bit7 |  V   |   V  |
         * |----------------------------------------
         * | bit8 |      |      |
         * | bit9 |  |   |   |  |
         * | bit10|  V   |   V  |
         * | bit11|      ^      ^
         * | bit12|  --> |   -> |
         * |----------------------------------------------
         * | bit13|      |      |
         * | bit14|      |      |
         * | bit15|      |      |
         * | .    |      |      |
         * | .    |      |      |
         * | .    |      |      |
         * \------------------------------------------
         * It can be seen from the above, we write bit0~bit7 of a byte at first, and then,
         * we write next segment immediately after bit8~bit11 is written.
         */
        for (uint8_t mask = 0x01; mask != 0; mask <<= 1) {
            this->drawPoint(x, y, this->mode ? (chrOfDisplay & mask) : ((chrOfDisplay & mask) ? 0 : 1));
            y++;
            if (y - baseY >= height) {
                y = baseY;
                x++;
                break;
            }
        }
    }
}

/**
  * @brief  OLED display a format string
  *
  * @note
  * @param  x   Horizontal coordinates, [0:63]
  * @param  y   Vertical coordinates, [0:127]
  * @param  format format string
 */
static void OLED_printf(OLED *this, uint8_t x, uint8_t y, char *format, ...) {
    char result[16];
    va_list arg;
    va_start(arg, format);
    vsprintf(result, format, arg);
    va_end(arg);
    this->showString(this, x, y, result);
}

/**
 * @brief  OLED draw point on the screen
 *
 * @note
 * @param  x     Horizontal coordinates, [0:63]
 * @param  y     Vertical coordinates, [0:127]
 * @param  point set or reset point
 * @retval
 */
static void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t point) {
    uint8_t bitOfByte = 1 << (y % 8);
    if (point) {
        OLED_GRAPHIC_MEMORY[y / 8][x] |= bitOfByte;  // Set point
    } else {
        OLED_GRAPHIC_MEMORY[y / 8][x] &= ~bitOfByte; // Reset point
    }
}

/**
  * @brief  OLED refresh Graphic Memory
  *
  * @note
  * @retval
  */
static void OLED_RefreshGram(OLED *this) {
    for (int page = 0; page < 8; ++page) {
        uint8_t dat[] = {0xB0 + page, 0x00, 0x10};
        this->oledProtocol->write(this->oledProtocol, OLED_I2C_ADDRESS, OLED_I2C_CMD, dat, 3);
        this->oledProtocol->write(this->oledProtocol, OLED_I2C_ADDRESS, OLED_I2C_DATA, OLED_GRAPHIC_MEMORY[page], 128);
    }
}

/**
  * @brief  OLED次方函数
  * @retval 返回值等于X的Y次方
  */
static uint32_t OLED_Pow(uint32_t X, uint32_t Y) {
    uint32_t Result = 1;
    while (Y--) {
        Result *= X;
    }
    return Result;
}


static OLED *build(OLEDProtocol *oledProtocol, OLEDFont *oledFont) {
    OLED *oled = malloc(sizeof(OLED));

    Driver_add(oled, GEN_PERM);

    oled->size          = OLED_SIZE_16;
    oled->mode          = OLED_MODE_POSITIVE;
    oled->oledProtocol  = oledProtocol;
    oled->oledFont      = oledFont;

    oled->init          = &OLED_Init;
    oled->clear         = &OLED_Clear;
    oled->full          = &OLED_Full;
    oled->refresh       = &OLED_RefreshGram;

    oled->showChar      = &OLED_ShowChar;
    oled->showString    = &OLED_ShowString;
    oled->showNum       = &OLED_ShowNum;
    oled->showSignedNum = &OLED_ShowSignedNum;
    oled->showHexNum    = &OLED_ShowHexNum;
    oled->showBinNum    = &OLED_ShowBinNum;
    oled->showFloatNum  = &OLED_ShowFloatNum;

    oled->showChinese   = &OLED_showChinese;
    oled->showImage     = &OLED_showImage;
    oled->printf        = &OLED_printf;
    oled->drawPoint     = &OLED_DrawPoint;

    return oled;
}

const struct OLEDClass OLEDBuilder = {.build = &build};


